package com.coocaa.x.service.litecache;

import com.coocaa.x.service.litecache.data.common.LiteCacheObject;
import com.coocaa.x.service.litedb.data.client.LiteDBSupport;

import java.io.File;
import java.lang.Thread.UncaughtExceptionHandler;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.UUID;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * Created by lu on 15-3-13.
 */
public class LiteCacheHelper {
    public static final LiteCacheHelper instance = new LiteCacheHelper();

    public static LiteCacheHelper getInstance() {
        return instance;
    }

    private static final Map<String, LiteCacheTable> cache = new HashMap<String, LiteCacheTable>();
    private static final Map<String, LiteCacheObject> cacheMap = new HashMap<String, LiteCacheObject>();
    private static final Map<String, CacheRunnable> cachingRunnables = new HashMap<String, CacheRunnable>();
    private static final Map<String, Queue<LockedLiteCacheObject>> lockedQueues = new HashMap<String, Queue<LockedLiteCacheObject>>();

    public static interface LiteCacheHelperCachingListener {
        void onLiteCacheObjectCached(CacheRunnable crun, LiteCacheObject obj, boolean success,
                                     String failmsg, String extra);
    }

    public static interface LiteCacheHelperLockingListener {
        void onLiteCacheObjectLocked(LiteCacheObject obj, boolean success, String failmsg,
                                     String content);
    }

    public static abstract class CacheRunnable implements Runnable {
        private static ThreadPoolExecutor executor = null;

        static {
            executor = new ThreadPoolExecutor(3, 3, 10, TimeUnit.DAYS,
                    new LinkedBlockingQueue<Runnable>(), new ThreadFactory() {
                @Override
                public Thread newThread(Runnable r) {
                    Thread thread = new Thread(r, "DoCacheThread");
                    thread.setUncaughtExceptionHandler(new UncaughtExceptionHandler() {
                        @Override
                        public void uncaughtException(Thread thread, Throwable ex) {
                            ex.printStackTrace();
                        }
                    });
                    return thread;
                }
            });
            executor.prestartAllCoreThreads();
        }

        public static CacheRunnable create(String url, LiteCacheHelperCachingListener listener) {
            return new OnlineCacheRunnable(url, listener);
        }

        public static class CacheRunnableCallback {
            public LiteCacheHelperCachingListener listener;
            public CacheRunnable runnable;

            public CacheRunnableCallback(LiteCacheHelperCachingListener listener,
                                         CacheRunnable runnable) {
                this.listener = listener;
                this.runnable = runnable;
            }
        }

        private String id = UUID.randomUUID().toString();
        protected String url = null;
        private List<CacheRunnableCallback> listeners = new ArrayList<CacheRunnableCallback>();

        public CacheRunnable(String url, LiteCacheHelperCachingListener listener) {
            this.url = url;
            addLiteCacheHelperCachingListener(new CacheRunnableCallback(listener, this));
        }

        public String getID() {
            return id;
        }

        public void addLiteCacheHelperCachingListener(List<CacheRunnableCallback> listeners) {
            synchronized (listeners) {
                for (CacheRunnableCallback listener : listeners)
                    addLiteCacheHelperCachingListener(listener);
            }
        }

        public void addLiteCacheHelperCachingListener(CacheRunnableCallback listener) {
            synchronized (listeners) {
                listeners.add(listener);
            }
        }

        private boolean isInCaching() {
            boolean flag = false;
            synchronized (cachingRunnables) {
                CacheRunnable run = cachingRunnables.get(url);
                if (run != null) {
                    run.addLiteCacheHelperCachingListener(listeners);
                    flag = true;
                } else {
                    cachingRunnables.put(url, this);
                }
            }
            return flag;
        }

        private LiteCacheObject doCache() throws Exception {
            LiteCacheObject ret = null;
            LiteCacheTable mydata = null;
            synchronized (cache) {
                mydata = cache.get(url);
            }
            if (mydata != null && new File(mydata.cache_url).exists()) {
                synchronized (cacheMap) {
                    ret = cacheMap.get(url);
                    if (ret == null) {
                        ret = new LiteCacheObject(url);
                        cacheMap.put(url, ret);
                    }
                }
                mydata.recent_used = System.currentTimeMillis();
                mydata.update("url=?", mydata.url);
            } else {
                ret = new LiteCacheObject(url);
                mydata = doRealCache();
                mydata.insert();
                synchronized (cacheMap) {
                    cacheMap.put(url, ret);
                }
                synchronized (cache) {
                    cache.put(url, mydata);
                }
            }
            return ret;
        }

        protected abstract LiteCacheTable doRealCache() throws Exception;

        private void doCompleteSuccess(LiteCacheObject obj) {
            synchronized (cachingRunnables) {
                cachingRunnables.remove(url);
            }
            synchronized (listeners) {
                for (CacheRunnableCallback listener : listeners)
                    listener.listener.onLiteCacheObjectCached(listener.runnable,
                            (LiteCacheObject) obj.clone(), true, "", "");
            }
        }

        private void doCompleteFailed(String msg) {
            synchronized (cachingRunnables) {
                cachingRunnables.remove(url);
            }
            synchronized (listeners) {
                for (CacheRunnableCallback listener : listeners)
                    listener.listener.onLiteCacheObjectCached(listener.runnable,
                            new LiteCacheObject(url), false, msg, "");
            }
        }

        @Override
        public void run() {
            if (!isInCaching()) {
                LiteCacheObject obj = null;
                synchronized (cacheMap) {
                    obj = cacheMap.get(url);
                }
                if (obj == null) {
                    try {
                        obj = doCache();
                        synchronized (cacheMap) {
                            cacheMap.put(url, obj);
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                        doCompleteFailed(e.getMessage());
                        return;
                    }
                }
                doCompleteSuccess(obj);
            }
        }

        public void start() {
            executor.execute(this);
        }
    }

    private static class LockedLiteCacheObject {
        public LiteCacheObject obj = null;
        public LiteCacheHelperLockingListener listener = null;

        public LockedLiteCacheObject(LiteCacheObject obj, LiteCacheHelperLockingListener listener) {
            this.obj = obj;
            this.listener = listener;
        }
    }

    public static class LiteCacheTable extends LiteDBSupport {
        public String url = null;
        public String cache_url = null;
        public long recent_used = 0;

        public LiteCacheTable() throws Exception {
            super("tb_litecache", 1);
        }
    }

    private boolean bInited = false;

    private LiteCacheHelper() {
        loadDB();
    }

    private void loadDB() {
        LiteCacheObject.postAction(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        LiteCacheTable table = new LiteCacheTable();
                        List<LiteDBSupport> list = table.selectAll();
                        for (LiteDBSupport data : list) {
                            LiteCacheTable mydata = (LiteCacheTable) data;
                            synchronized (cache) {
                                cache.put(mydata.url, mydata);
                            }
                        }
                        synchronized (LiteCacheHelper.this) {
                            bInited = true;
                            LiteCacheHelper.this.notifyAll();
                        }
                        break;
                    } catch (Exception e) {
                        e.printStackTrace();
                        try {
                            Thread.sleep(1);
                        } catch (InterruptedException e1) {
                            e1.printStackTrace();
                        }
                    }
                }
            }
        });
    }

    public CacheRunnable cache(String url, LiteCacheHelperCachingListener listener) {
        synchronized (LiteCacheHelper.this) {
            if (!bInited)
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
        }
        return CacheRunnable.create(url, listener);
    }

    public void lock(final LiteCacheObject obj, final LiteCacheHelperLockingListener listener) {
        synchronized (LiteCacheHelper.this) {
            if (!bInited)
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
        }
        String content = null;

        synchronized (cache) {
            LiteCacheTable o = cache.get(obj.url);
            content = o.cache_url;
        }
        synchronized (cacheMap) {
            if (!cacheMap.containsKey(obj.url)) {
                listener.onLiteCacheObjectLocked(obj, false, "", "");
                return;
            }
            if (tryLock(obj, listener)) {
                listener.onLiteCacheObjectLocked(obj, true, "", content);
            }
        }
    }

    private boolean tryLock(LiteCacheObject obj, LiteCacheHelperLockingListener listener) {
        Queue<LockedLiteCacheObject> locked = null;
        synchronized (lockedQueues) {
            locked = lockedQueues.get(obj.url);
            if (locked == null) {
                locked = new LinkedList<LockedLiteCacheObject>();
                lockedQueues.put(obj.url, locked);
            }
        }
        boolean ret = false;
        synchronized (locked) {
            if (locked.size() == 0) {
                ret = true;
                locked.offer(new LockedLiteCacheObject(obj, listener));
            } else if (locked.size() > 0 && locked.peek().obj.equals(obj)) {
                ret = true;
            } else
                locked.offer(new LockedLiteCacheObject(obj, listener));
        }
        return ret;
    }

    public void unlock(LiteCacheObject obj) {
        synchronized (LiteCacheHelper.this) {
            if (!bInited)
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
        }
        Queue<LockedLiteCacheObject> locked = null;
        synchronized (lockedQueues) {
            locked = lockedQueues.get(obj.url);
            if (locked == null || locked.size() == 0) {
                return;
            }
        }
        synchronized (locked) {
            if (locked.size() > 0 && locked.peek().obj.equals(obj)) {
                locked.poll();
                if (locked.size() > 0)
                    lock(locked.peek().obj, locked.peek().listener);
            } else {
                for (LockedLiteCacheObject o : locked) {
                    if (o.equals(obj)) {
                        locked.remove(o);
                        break;
                    }
                }
            }
        }
    }
}
